package bg.btanev.eightyfour.domain.profile;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import bg.btanev.eightyfour.domain.Tag;
import bg.btanev.eightyfour.domain.Target;
import bg.btanev.eightyfour.domain.entitybase.DomainEntity;
import bg.btanev.eightyfour.domain.exception.ProfileUpdateException;
import bg.btanev.eightyfour.shared.state.ProfileType;
import bg.btanev.eightyfour.shared.state.ProfileUpdateStatus;

/**
 *
 * @author B.Tanev
 *
 */
public abstract class Profile<PE extends ProfileEvent> extends DomainEntity<Long> {
	/**
	 *
	 */
	private static final long serialVersionUID = -4701937901574390229L;

	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory.getLogger(Profile.class);

	private String name; // not-null; unique profile name
	// one-to-many; cascade save-update, delete, delete-orphan; inverse=true
	private SortedSet<PE> profileEvents = new TreeSet<PE>();
	private Set<Tag> tags = new HashSet<Tag>();

	private Date lastUpdate;
	private ProfileUpdateStatus upateStatus;
	private ProfileType type; // not-null;

	private Target target; // reference to parent target; many-to-one

	private transient final ProfileEventsUpdateStrategy<Profile<PE>, PE> updateStrategy;

	// initialization block
	{
		updateStrategy = createProfileEventsUpdateStrategy();

		Validate.notNull(updateStrategy, "Profile events update strategy cannot be null.");
	}

	protected Profile() {
		super();
	}

	public Profile(String name, ProfileType type, Target target) {
		this(null, name, type, target);
	}

	protected Profile(Long id, String name, ProfileType type, Target target) {
		super(id);

		Validate.notNull(target, "Target cannot be null.");
		Validate.notNull(type);

		setName(name);
		this.type = type;

		updateStatus(ProfileUpdateStatus.INITIALIZED);
		updateTarget(target);
	}

	public void setName(String name) {
		Validate.notEmpty(name, "Profile name cannot be empty");
		this.name = name;
	}

	public void updateStatus(ProfileUpdateStatus newStatus) {
		logger.debug("updateStatus(ProfileUpdateStatus) - start"); //$NON-NLS-1$

		Validate.notNull(newStatus);

		this.upateStatus = newStatus;
		logger.info("updateStatus(ProfileUpdateStatus) - ProfileUpdateStatus upateStatus=" + upateStatus); //$NON-NLS-1$

		if (this.upateStatus == ProfileUpdateStatus.SUCCESSFUL) {
			this.lastUpdate = new Date();
			logger.info("updateStatus(ProfileUpdateStatus) - Date lastUpdate=" + lastUpdate); //$NON-NLS-1$
		}

		logger.debug("updateStatus(ProfileUpdateStatus) - end"); //$NON-NLS-1$
	}

	public ProfileType getType() {
		return type;
	}

	public Date getLastUpdate() {
		return lastUpdate;
	}

	public ProfileUpdateStatus getUpateStatus() {
		return upateStatus;
	}

	/**
	 *
	 * @param <S>
	 * @return
	 */
	protected abstract <S extends ProfileEventsUpdateStrategy<? extends Profile<PE>, PE>> S createProfileEventsUpdateStrategy();

	public String getName() {
		return name;
	}

	public boolean addTag(Tag tag) {
		Validate.notNull(tag, "Tag cannot be null");

		return tags.add(tag);
	}

	public boolean addTags(Collection<Tag> tags) {
		Validate.notNull(tags, "Tags cannot be null");

		return this.tags.addAll(tags);
	}

	public boolean removeTag(Tag tag) {
		Validate.notNull(tag, "Tag cannot be null");

		return tags.remove(tag);
	}

	public Set<Tag> getTags() {
		return Collections.unmodifiableSet(tags);
	}

	@SuppressWarnings("unchecked")
	public <T extends ProfileEvent> boolean addProfileEvent(T profileEvent) {
		logger.debug("addProfileEvent(T) - start"); //$NON-NLS-1$

		Validate.notNull(profileEvent, "Profile event cannot be null");

		if (!(this.equals(profileEvent.getProfile()))) {
			throw new IllegalArgumentException("Profile event must be of same profile.");
		}

		boolean returnboolean = profileEvents.add((PE) profileEvent);
		logger.debug("addProfileEvent(T) - end"); //$NON-NLS-1$

		return returnboolean;
	}

	public boolean addProfileEvents(Collection<PE> profileEvents) {
		Validate.notNull(profileEvents, "Profile events cannot be null.");

		boolean result = false;
		for (PE pe : profileEvents) {
			result = result | this.addProfileEvent(pe);
		}

		return result;
	}

	public <T extends ProfileEvent> boolean removeProfileEvent(T profileEvent) {
		Validate.notNull(profileEvent, "Profile event cannot be null");

		return profileEvents.remove(profileEvent);
	}

	public int getProfileEventsCount() {
		return profileEvents.size();
	}

	public SortedSet<PE> getProfileEvents() {
		return Collections.unmodifiableSortedSet(profileEvents);
	}

	public Target getTarget() {
		return target;
	}

	private boolean updateTarget(Target target) {
		assert (target != null);

		if (this.target != null) {
			this.target.removeProfile(this);
		}

		this.target = target;
		return this.target.addProfile(this);
	}

	/**
	 *
	 * @return a {@link List} of the current online {@link ProfileEvent}s
	 * @throws ProfileUpdateException
	 */
	public List<PE> fetchOnlineProfileEvents() throws ProfileUpdateException {
		logger.debug("fetchOnlineProfileEvents() - start"); //$NON-NLS-1$

		List<PE> returnList = updateStrategy.retrieveOnlineProfileEvents(this);

		logger.debug("fetchOnlineProfileEvents() - end"); //$NON-NLS-1$

		return returnList;
	}

	/**
	 * Fetches the online {@link ProfileEvent} data and merges it into the current {@link ProfileEvent}s data.
	 *
	 * @throws ProfileUpdateException
	 */
	public void updateProfileEvents() throws ProfileUpdateException {
		logger.debug("updateProfileEvents() - start"); //$NON-NLS-1$

		updateProfileEvents(fetchOnlineProfileEvents());

		logger.debug("updateProfileEvents() - end"); //$NON-NLS-1$
	}

	/**
	 *
	 * @param profileEventsToAdd
	 *            {@link ProfileEvent}s to add to the current list.
	 */
	public void updateProfileEvents(List<PE> profileEventsToAdd) {
		logger.debug("updateProfileEvents(List<PE>) - start"); //$NON-NLS-1$

		if (profileEventsToAdd != null && profileEventsToAdd.size() > 0) {
			updateStrategy.mergeProfileEvents(profileEvents, profileEventsToAdd);
		}

		logger.debug("updateProfileEvents(List<PE>) - end"); //$NON-NLS-1$
	}

	@Override
	public String toString() {
		return "Profile [lastUpdate=" + lastUpdate + ", name=" + name + ", upateStatus=" + upateStatus
				+ ", toString()=" + super.toString() + "]";
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((lastUpdate == null) ? 0 : lastUpdate.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + ((upateStatus == null) ? 0 : upateStatus.hashCode());
		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Profile other = (Profile) obj;
		if (lastUpdate == null) {
			if (other.lastUpdate != null)
				return false;
		} else if (!lastUpdate.equals(other.lastUpdate))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (upateStatus == null) {
			if (other.upateStatus != null)
				return false;
		} else if (!upateStatus.equals(other.upateStatus))
			return false;
		return true;
	}
}
